﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace SmartPlant.WinCore.Helper
{
    /// <summary>
    /// 目录、文件与文件内容相关，常用的处理方法集合。
    /// </summary>
    public class FileHelper
    {
        private static IDictionary<string, string> dics = new Dictionary<string, string>();
        static FileHelper()
        {
            dics.Add("FFD8FFE0", ".jpg");
            dics.Add("89504E47", ".png");
            dics.Add("47494638", ".gif");
            dics.Add("49492A00", ".tif");
            dics.Add("424D", ".bmp");

            //PS和CAD
            dics.Add("38425053", ".psd");
            dics.Add("41433130", ".dwg"); // CAD
            dics.Add("252150532D41646F6265", ".ps");

            //办公文档类
            dics.Add("D0CF11E0", ".doc"); //ppt、doc、xls
            dics.Add("504B0304", ".docx");//pptx、docx、xlsx    

            /**注意由于文本文档录入内容过多，则读取文件头时较为多变-START**/
            dics.Add("0D0A0D0A", ".txt");//txt
            dics.Add("0D0A2D2D", ".txt");//txt
            dics.Add("0D0AB4B4", ".txt");//txt        
            dics.Add("B4B4BDA8", ".txt");//文件头部为汉字
            dics.Add("73646673", ".txt");//txt,文件头部为英文字母
            dics.Add("32323232", ".txt");//txt,文件头部内容为数字
            dics.Add("0D0A09B4", ".txt");//txt,文件头部内容为数字
            dics.Add("3132330D", ".txt");//txt,文件头部内容为数字      
            /**注意由于文本文档录入内容过多，则读取文件头时较为多变-END**/


            dics.Add("7B5C727466", ".rtf"); // 日记本

            dics.Add("255044462D312E", ".pdf");

            //视频或音频类
            dics.Add("3026B275", ".wma");
            dics.Add("57415645", ".wav");
            dics.Add("41564920", ".avi");
            dics.Add("4D546864", ".mid");
            dics.Add("2E524D46", ".rm");
            dics.Add("000001BA", ".mpg");
            dics.Add("000001B3", ".mpg");
            dics.Add("6D6F6F76", ".mov");
            dics.Add("3026B2758E66CF11", ".asf");

            //压缩包
            dics.Add("52617221", ".rar");
            dics.Add("1F8B08", ".gz");

            //程序文件
            dics.Add("3C3F786D6C", ".xml");
            dics.Add("68746D6C3E", ".html");
            dics.Add("7061636B", ".java");
            dics.Add("3C254020", ".jsp");
            dics.Add("4D5A9000", ".exe");


            dics.Add("44656C69766572792D646174653A", ".eml"); // 邮件
            dics.Add("5374616E64617264204A", ".mdb");//Access数据库文件

            dics.Add("46726F6D", ".mht");
            dics.Add("4D494D45", ".mhtml");
        }

        public static string GetFileType(Stream stream)
        {
            byte[] b = new byte[4];
            if (stream != null)
            {
                try
                {
                    stream.Read(b, 0, b.Length);
                }
                catch (IOException e)
                {
                    LoggerHelper.Current.Error($"读取文件流类型失败：{e.Message}", e);
                    return string.Empty;
                }
            }
            string fileKey = GetFileHeader(b);

            if (dics.ContainsKey(fileKey))
                return dics[fileKey];
            else
            {
                LoggerHelper.Current.Error("未找到当前流的扩展名");
                return string.Empty;
            }
        }
        /**
         * 根据文件转换成的字节数组获取文件头信息
         * 
         * @param filePath
         *            文件路径
         * @return 文件头信息
         */
        private static string GetFileHeader(byte[] b)
        {
            string value = BytesToHexString(b);
            return value;
        }

        /**
         * 将要读取文件头信息的文件的byte数组转换成string类型表示
         * 下面这段代码就是用来对文件类型作验证的方法， 
         * 将字节数组的前四位转换成16进制字符串，并且转换的时候，要先和0xFF做一次与运算。
         * 这是因为，整个文件流的字节数组中，有很多是负数，进行了与运算后，可以将前面的符号位都去掉，
         * 这样转换成的16进制字符串最多保留两位，如果是正数又小于10，那么转换后只有一位，
         * 需要在前面补0，这样做的目的是方便比较，取完前四位这个循环就可以终止了
         * @param src要读取文件头信息的文件的byte数组
         * @return 文件头信息
         */
        private static string BytesToHexString(byte[] src)
        {
            StringBuilder builder = new StringBuilder();
            if (src == null || src.Length <= 0)
            {
                return null;
            }
            string hv;
            for (int i = 0; i < src.Length; i++)
            {
                // 以十六进制（基数 16）无符号整数形式返回一个整数参数的字符串表示形式，并转换为大写
                hv = Convert.ToString(src[i] & 0xFF, 16).ToUpper();
                if (hv.Length < 2)
                {
                    builder.Append(0);
                }
                builder.Append(hv);
            }
            return builder.ToString();
        }

        /// <summary>
        /// 获取指定目录的文件信息。
        /// </summary>
        /// <param name="path"></param>
        /// <param name="searchPattern"></param>
        /// <returns></returns>
        public static List<FileDataInfo>? GetFiles(string path, string searchPattern)
        {
            DirectoryInfo dir = new DirectoryInfo(path);
            if (!dir.Exists) return null;
            var files = dir.GetFiles(searchPattern, SearchOption.TopDirectoryOnly);
            if (files != null && files.Length > 0)
            {
                List<FileDataInfo> fileDataInfos = new List<FileDataInfo>();
                foreach (var item in files)
                {
                    var fileDataInfo = new FileDataInfo
                    {
                        FileName = item.Name.RightRemoveString(item.Extension.Length),
                        FullName = item.FullName,
                        ExtName = item.Extension,
                        Size = item.Length / 1024,
                        SizeUnit = "KB"
                    };
                    fileDataInfos.Add(fileDataInfo);
                }
                return fileDataInfos;
            }
            return null;
        }

        #region Path & Directory & File
        /// <summary>
        /// 确保目录存在。
        /// <para>如果目录不存在，则创建目录（包括上级目录）。</para>
        /// </summary>
        /// <param name="path">目录路径（不含文件名）</param>
        /// <returns>返回一个 Boolean 值，如果目录不存在且创建目录出现异常时，返回值为 False。</returns>
        public static bool EnsureDir(string path)
        {
            try
            {
                if (Directory.Exists(path))
                    return true;
                if (EnsureDir(Directory.GetParent(path).ToString()))
                {
                    Directory.CreateDirectory(path);
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"目录创建错误：{ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 确保文件存在。
        /// <para>如果文件目录不存在，则创建目录（包括上级目录）。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <returns>返回一个 Boolean 值，如果目录或文件不存在且创建它们出现异常时，返回值为 False。</returns>
        public static bool EnsureFile(string path)
        {
            try
            {
                if (File.Exists(path))
                    return true;
                if (EnsureDir(Directory.GetParent(path).ToString()))
                {
                    File.Create(path).Close();
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件创建错误：{ex.Message}");
                return false;
            }
        }

        #endregion

        #region Text 相关

        /// <summary>
        /// 将默认编码类型（Unicode）的字符串，追加至指定文件。
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// <para>方法将确保目录与文件存在</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="text">需要写入的字符</param>
        /// <param name="isNewLine">指定是否将字符串写入新行</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入字符时）出现异常，返回值为 False。</returns>
        public static bool AddFileText(string path, string text, bool isNewLine)
        {
            return AddFileText(path, Encoding.Unicode, text, isNewLine);
        }

        /// <summary>
        /// 将指定编码类型的字符串，追加至指定文件。
        /// <para>方法将确保目录与文件存在。</para>
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="text">需要写入的字符</param>
        /// <param name="isNewLine">指定是否将字符串写入新行</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入字符时）出现异常，返回值为 False。</returns>
        public static bool AddFileText(string path, Encoding encodingType, string text, bool isNewLine)
        {
            if (!EnsureFile(path))
                return false;

            try
            {
                using var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
                using var sw = new StreamWriter(fs, encodingType);
                if (isNewLine)
                    sw.WriteLine(text);
                else
                    sw.Write(text);
                return true;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件写入错误：{ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 将默认编码类型（Unicode）的字符串，写入指定文件。
        /// <para>方法将确保目录与文件存在。</para>
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="text">需要写入的字符</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入字符时）出现异常，返回值为 False。</returns>
        public static bool WriteFileText(string path, string text)
        {
            return WriteFileText(path, Encoding.Unicode, text);
        }

        /// <summary>
        /// 将指定编码类型的字符串，写入指定文件。
        /// <para>方法将确保目录与文件存在。</para>
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="text">需要写入的字符</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入字符时）出现异常，返回值为 False。</returns>
        public static bool WriteFileText(string path, Encoding encodingType, string text)
        {
            if (!EnsureDir(Directory.GetParent(path).ToString()))
                return false;

            var fi = new FileInfo(path);
            if (fi.Exists)
                fi.Delete();

            using var fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
            using var sw = new StreamWriter(fs, encodingType);
            sw.Write(text);
            sw.Flush();
            sw.Close();
            fs.Close();
            return true;
        }


        /// <summary>
        /// 读取指定的文件流至内存。
        /// </summary>
        /// <param name="filePath">物理地址</param>
        public static string ReadFileText(string srcFile)
        {
            try
            {
                using var srcStream = File.OpenRead(srcFile);
                using TextReader reader = new StreamReader(srcStream);
                string str = reader.ReadToEnd();
                reader.Close();
                srcStream.Close();
                return str;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件读取错误：{ex.Message}");
                return string.Empty;
            }
        }

        /// <summary>
        /// 读取指定的文件流至内存。
        /// </summary>
        /// <param name="filePath">物理地址</param>
        public static async Task<string> ReadFileTextAsync(string srcFile)
        {
            try
            {
                using var srcStream = File.OpenRead(srcFile);
                using TextReader reader = new StreamReader(srcStream);
                string str = await reader.ReadToEndAsync();
                reader.Close();
                srcStream.Close();
                return str;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件读取错误：{ex.Message}");
                return string.Empty;
            }
        }

        /// <summary>
        /// 依据默认编码类型（Unicode），获取指定文件、指定范围的字符。
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="index">指定要获取内容的起始索引位置（例如文件内容：“类型 FileHelper 是...”，如果 index 设置为 3，则取得字符串“FileHelper 是...”）</param>
        /// <param name="count">指定要获取内容的长度，内容读取长度设置不宜过大，以避免内存溢出</param>
        /// <returns>返回 String 类型值，当文件不存在或其它异常时，返回值为 Null。</returns>
        public static string? ReadFileText(string path, int index, int count)
        {
            return ReadFileText(path, Encoding.Unicode, index, count);
        }

        /// <summary>
        /// 依据指定的编码类型，获取指定文件、指定范围的字符。
        /// <para>系统推荐使用默认的 Unicode 编码类型格式来进行文字的读取与写入。</para>
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="index">指定要获取内容的起始索引位置（例如文件内容：“类型 FileHelper 是...”，如果 index 设置为 3，则取得字符串“FileHelper 是...”）</param>
        /// <param name="count">指定要获取内容的长度，内容读取长度设置不宜过大，以避免内存溢出</param>
        /// <returns>返回 String 类型值，当文件不存在或其它异常时，返回值为 Null。</returns>
        public static string? ReadFileText(string path, Encoding encodingType, int index, int count)
        {
            try
            {
                if (!File.Exists(path))
                    return null;

                int maxReads = 10000;
                char[] tempChars = new char[maxReads];
                char[] chars = new char[count];
                int loopCount = index / maxReads;
                int tempIndex = index % maxReads;
                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    using var sr = new StreamReader(fs, encodingType);
                    // 如果起始索引超过 1W 个字符，则以 1W 个字符为单位，循环丢弃起始索引之前的所有字符，以避免内存开销过大。
                    while (loopCount-- > 0)
                        sr.ReadBlock(tempChars, 0, maxReads);
                    sr.ReadBlock(tempChars, 0, tempIndex);
                    sr.ReadBlock(chars, 0, count);
                    sr.Close();
                    fs.Close();
                }
                return new string(chars);
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件读取错误：{ex.Message}");
                return null;
            }
        }

        #endregion

        #region Binary 相关

        /// <summary>
        /// 基于默认编码类型（Unicode），将字节数组追加至指定的二进制文件
        /// </summary>
        /// <param name="path">二进制文件</param>
        /// <param name="bytes">字节数组</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入二进制数据时）出现异常，返回值为 False。</returns>
        public static bool AddFileBinary(string path, byte[] bytes)
        {
            return AddFileBinary(path, Encoding.Unicode, bytes);
        }

        /// <summary>
        /// 基于所提供编码类型，将字节数组追加至指定的二进制文件
        /// </summary>
        /// <param name="path">二进制文件</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="bytes">字节数组</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入二进制数据时）出现异常，返回值为 False。</returns>
        public static bool AddFileBinary(string path, Encoding encodingType, byte[] bytes)
        {
            if (!EnsureDir(Directory.GetParent(path).ToString()))
                return false;

            try
            {
                using FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
                using BinaryWriter bw = new BinaryWriter(fs, encodingType);
                bw.Write(bytes);
                bw.Flush();
                bw.Close();
                return true;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件写入错误：{ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 基于默认编码类型（Unicode），将字节数组写入指定的二进制文件
        /// </summary>
        /// <param name="path">二进制文件</param>
        /// <param name="bytes">字节数组</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入二进制数据时）出现异常，返回值为 False。</returns>
        public static bool WriteFileBinary(string path, byte[] bytes)
        {
            return WriteFileBinary(path, Encoding.Unicode, bytes);
        }

        /// <summary>
        /// 基于所提供编码类型，将字节数组写入指定的二进制文件
        /// </summary>
        /// <param name="path">二进制文件</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="bytes">字节数组</param>
        /// <returns>返回一个 Boolean 值，如果指定的目录、文件不存在且创建它们（或写入二进制数据时）出现异常，返回值为 False。</returns>
        public static bool WriteFileBinary(string path, Encoding encodingType, byte[] bytes)
        {
            if (!EnsureDir(Directory.GetParent(path).ToString()))
                return false;

            try
            {
                using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
                using var bw = new BinaryWriter(fs, encodingType);
                bw.Write(bytes);
                bw.Flush();
                bw.Close();
                return true;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件写入错误：{ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 依据默认编码类型（Unicode），获取指定文件、指定范围的二进制数据。
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="index">指定要获取数据的起始索引位置</param>
        /// <param name="count">指定要获取数据的长度，内容读取长度设置不宜过大，以避免内存溢出</param>
        /// <returns>返回 byte[] 类型值，当文件不存在或其它异常时，返回值为 Null。</returns>
        public static byte[]? ReadFileBinary(string path, int index, int count)
        {
            return ReadFileBinary(path, Encoding.Unicode, index, count);
        }

        /// <summary>
        /// 依据指定的编码类型，获取指定文件、指定范围的二进制数据。
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="encodingType">编码类型</param>
        /// <param name="index">指定要获取数据的起始索引位置</param>
        /// <param name="count">指定要获取数据的长度，内容读取长度设置不宜过大，以避免内存溢出</param>
        /// <returns>返回 byte[] 类型值，当文件不存在或其它异常时，返回值为 Null。</returns>
        public static byte[]? ReadFileBinary(string path, Encoding encodingType, int index, int count)
        {
            try
            {
                if (!File.Exists(path))
                    return null;

                int maxReads = 1024000;
                byte[] bytes = new byte[count];
                int loopCount = index / maxReads;
                int tempIndex = index % maxReads;
                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    using BinaryReader br = new BinaryReader(fs, encodingType);
                    // 如果起始索引超过1M（1000K），则以1M为单位，循环丢弃起始索引之前的所有字节，以避免内存开销过大。
                    while (loopCount-- > 0)
                        br.ReadBytes(maxReads);
                    br.ReadBytes(tempIndex);
                    bytes = br.ReadBytes(count);
                    br.Close();
                    fs.Close();
                }
                return bytes;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件读取错误：{ex.Message}");
                return null;
            }
        }

        #endregion


        /// <summary>
        /// 将远程文件流下载至本地下载文件
        /// </summary>
        /// <param name="filePath">物理地址</param>
        public static async Task<bool> WriteFileStreamAsync(Stream srcStream, string targetFilePath)
        {

            try
            {
                var fileInfo = new FileInfo(targetFilePath);
                if (!fileInfo.Directory.Exists)
                    fileInfo.Directory.Create();
                if (fileInfo.Exists)
                    fileInfo.Delete();
                srcStream.Position = 0;
                using Stream fsWriteStream = File.OpenWrite(targetFilePath);
                //构建写文件流对象
                using var fsWrite = new BinaryWriter(fsWriteStream); //没有找到就创建
                                                                     //开辟临时缓存内存
                byte[] byteArrayRead = new byte[1024 * 1024]; // 1字节*1024 = 1k 1k*1024 = 1M内存

                //通过死缓存去读文本中的内容
                while (true)
                {
                    //readCount 这个是保存真正读取到的字节数
                    int readCount = await srcStream.ReadAsync(byteArrayRead, 0, byteArrayRead.Length);

                    //开始写入读取到缓存内存中的数据到目标文本文件中
                    fsWrite.Write(byteArrayRead, 0, readCount);


                    //既然是死循环 那么什么时候我们停止读取文本内容 我们知道文本最后一行的大小肯定是小于缓存内存大小的
                    if (readCount < byteArrayRead.Length)
                    {
                        break; //结束循环
                    }
                }
                fsWrite.Flush();
                fsWrite.Close();
                return true;
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件写入错误：{ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 读取指定的文件流至内存。
        /// </summary>
        /// <param name="filePath">物理地址</param>
        public static async Task<byte[]?> ReadFileContentAsync(string srcFile)
        {
            using var srcStream = File.OpenRead(srcFile);
            return await ReadFileContentAsync(srcStream);
        }


        /// <summary>
        /// 读取指定的文件流至内存。
        /// </summary>
        /// <param name="filePath">物理地址</param>
        public static async Task<byte[]?> ReadFileContentAsync(Stream srcStream)
        {
            try
            {
                srcStream.Position = 0;
                using var fsWriteStream = new MemoryStream();
                //构建写文件流对象
                using (var fsWrite = new BinaryWriter(fsWriteStream)) //没有找到就创建
                {
                    //开辟临时缓存内存
                    byte[] byteArrayRead = new byte[1024 * 1024]; // 1字节*1024 = 1k 1k*1024 = 1M内存

                    //通过死缓存去读文本中的内容
                    while (true)
                    {
                        //readCount 这个是保存真正读取到的字节数
                        int readCount = await srcStream.ReadAsync(byteArrayRead, 0, byteArrayRead.Length);

                        //开始写入读取到缓存内存中的数据到目标文本文件中
                        fsWrite.Write(byteArrayRead, 0, readCount);


                        //既然是死循环 那么什么时候我们停止读取文本内容 我们知道文本最后一行的大小肯定是小于缓存内存大小的
                        if (readCount < byteArrayRead.Length)
                        {
                            break; //结束循环
                        }
                    }
                    return fsWriteStream.ToArray();
                }
            }
            catch (Exception ex)
            {
                LoggerHelper.Current.Error($"文件写入错误：{ex.Message}");
                return null;
            }
        }
    }

    public class FileDataInfo
    {
        public string IconGlyph => "&#xe660;";
        public string FileName { get; set; }
        public string FullName { get; set; }
        public string ExtName { get; set; }
        public long Size { get; set; }
        public string SizeUnit { get; set; }

        public override string ToString()
        {
            return FileName;
        }
    }
}